﻿// Deep Earth is a community project available under the Microsoft Public License (Ms-PL)
// Code is provided as is and with no warranty – Use at your own risk
// View the project and the latest code at http://DeepEarth.codeplex.com/

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;


namespace DeepEarth.Client.MapControl.Events
{
    /// <summary>
    /// <para>
    /// Provides events for the map which include the following:
    /// </para>
    /// <list type="bullet">
    /// <item>Mouse</item>
    /// <item>Keyboard</item>
    /// <item>Map Specific</item>
    /// </list>
    /// <example>
    /// 
    /// <code title="Register for the zoom changed event. When the event is raised, write out the the zoomlevel to the console">   
    /// MapInstance.Events.MapZoomChanged += Events_MapZoomChanged;
    /// 
    /// private void Events_MapZoomChanged(object sender, double zoomLevel)
    /// {
    ///   Console.WriteLine(zoomLevel.ToString());
    /// }
    /// </code>
    /// </example>
    /// </summary>
    public class MapEvents
    {
        private long _lastClick;
        private double _lastZoom;
        private bool _inZoom;
        private bool _isMouseDown;
        private readonly Map _map;
        private Point _priorOrigin;

        private const double ChangeSignificanceThreshold = 0.05;
        private const double MouseDoubleClickSpeed = 3000000;
        private const double MouseDragDelay = 2000000.0;

        /// <summary>
        /// MapEvents constructor, will use the specific instance of the map
        /// </summary>
        public MapEvents(Map map)
        {
            _map = map;
        }

        /// <summary>
        /// Toggles keyboard, mouse click, map zoom and mouse wheel events
        /// </summary>
        public bool Enable
        {
            get
            {
                return _isEnbled;
            }
            set
            {
                _isEnbled = value;
                EnableKeyboard = _isEnbled;
                EnableMouseClicks = _isEnbled;
                EnableMouseWheel = _isEnbled;
                EnableMapZoom = _isEnbled;
            }
        }
        bool _isEnbled = true;

        /// <summary>
        /// Toggles mouse down, mouse up, double click and drag events that the map will use
        /// </summary>
        public bool EnableMouseClicks
        {
            get { return _enableMouseClicks; }
            set { _enableMouseClicks = value; }
        }
        bool _enableMouseClicks = true;

        /// <summary>
        /// Toggles keyUp and keyDown events that the map will use
        /// </summary>
        public bool EnableKeyboard
        {
            get { return _enableKeyboard; }
            set { _enableKeyboard = value; }
        }
        bool _enableKeyboard = true;


        /// <summary>
        /// Toggles mouse wheel events that the map will use
        /// </summary>
        public bool EnableMouseWheel
        {
            get { return _enableMouseWheel; }
            set { _enableMouseWheel = value; }
        }
        bool _enableMouseWheel = true;

        /// <summary>
        /// Toggles map zoom events that the map will use
        /// </summary>
        public bool EnableMapZoom
        {
            get { return _enableMapZoom; }
            set { _enableMapZoom = value; }
        }
        bool _enableMapZoom = true;

        /// <summary>
        /// Toggles map zoom on double click events that the map will use
        /// </summary>
        public bool EnableMapZoomOnDoubleClick
        {
            get { return _enableMapZoomOnDoubleClick; }
            set { _enableMapZoomOnDoubleClick = value; }
        }
        bool _enableMapZoomOnDoubleClick = true;


        #region Key Events

        ///<summary>
        /// Delegate for handling common keyboard events.
        ///</summary>
        ///<param name="sender">Instance of Map</param>
        ///<param name="args">KeyEventArgs</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public delegate void MapKeyEvent(object sender, KeyEventArgs args);

        /// <summary>
        /// Fires when a key is pressed down
        /// </summary>
        public event MapKeyEvent MapKeyDown;

        /// <summary>
        /// Fires when a key is released
        /// </summary>
        public event MapKeyEvent MapKeyUp;
        internal void KeyDown(object sender, KeyEventArgs args)
        {
            if (EnableKeyboard)
            {
                if (MapKeyDown != null) MapKeyDown(sender, args);
            }
        }
        internal void KeyUp(object sender, KeyEventArgs args)
        {
            if (EnableKeyboard)
            {
                if (MapKeyUp != null) MapKeyUp(sender, args);
            }
        }

        #endregion


        #region Mouse Button Events

        ///<summary>
        /// Delegate for handling common mouse button events.
        ///</summary> 
        ///<param name="sender">Instance of Map</param>
        ///<param name="args">MouseButtonEventArgs</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public delegate void MapMouseButtonEvent(object sender, MouseButtonEventArgs args);

        /// <summary>
        /// Fires when the left mouse button is pressed down
        /// </summary>
        public event MapMouseButtonEvent MapMouseDown;

        /// <summary>
        /// Fires when the left mouse button is released
        /// </summary>
        public event MapMouseButtonEvent MapMouseUp;

        /// <summary>
        /// Fires if the delta between the mouseDown event is close enough to act as a double click
        /// </summary>
        public event MapMouseButtonEvent MapDoubleClick;

        internal void MouseDown(object sender, MouseButtonEventArgs args)
        {
            var map = (Map)sender;

            _isMouseDown = true;
            if (EnableMouseClicks)
            {
                RaiseIfDoubleClick(map, args);
                if (MapMouseDown != null) MapMouseDown(map, args);
            }
            _lastClick = DateTime.Now.Ticks;
        }

        private void RaiseIfDoubleClick(object sender, MouseButtonEventArgs args)
        {
            var map = (Map)sender;

            if (EnableMouseClicks)
            {
                if ((DateTime.Now.Ticks - _lastClick) < MouseDoubleClickSpeed)
                {
                    if (MapDoubleClick != null) MapDoubleClick(map, args);
                }
            }
        }


        internal void MouseUp(object sender, MouseButtonEventArgs args)
        {
            var map = (Map)sender;

            _isMouseDown = false;
            if (EnableMouseClicks)
            {
                if (MapMouseUp != null) MapMouseUp(map, args);
            }
        }




        #endregion


        #region Mouse Events

        ///<summary>
        /// Delegate for raising basic mouse events.
        ///</summary>
        ///<param name="sender">Instance of Map</param>
        ///<param name="args">MouseEventArgs</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public delegate void MapMouseEvent(object sender, MouseEventArgs args);

        /// <summary>
        /// Fires when the mouse moves while over the map
        /// </summary>
        public event MapMouseEvent MapMouseMove;

        /// <summary>
        /// Fires when the mouse initially enters the map
        /// </summary>
        public event MapMouseEvent MapMouseEnter;

        /// <summary>
        /// Fires when the mouse initially leaves the map
        /// </summary>
        public event MapMouseEvent MapMouseLeave;

        /// <summary>
        /// Fires when the mouse left button is held down and the mouse moves
        /// </summary>
        public event MapMouseEvent MapMouseDrag;

        internal void MouseMove(object sender, MouseEventArgs args)
        {
            var map = (Map)sender;
            if (MapMouseMove != null) {MapMouseMove(map, args);}

            RaiseIfMouseDrag(map, args);
        }

        internal void MouseEnter(object sender, MouseEventArgs args)
        {
            var map = (Map)sender;
            if (MapMouseEnter != null) MapMouseEnter(map, args);
        }

        internal void MouseLeave(object sender, MouseEventArgs args)
        {
            var map = (Map)sender;
            if (MapMouseLeave != null) MapMouseLeave(map, args);
        }

        private void RaiseIfMouseDrag(object sender, MouseEventArgs args)
        {
            if (EnableMouseClicks && _isMouseDown)
            {
                if (_lastClick != 0)
                {
                    var map = (Map)sender;
                    //If we are not zooming, go to drag right away, otherwise delay to not conflict with a double click zoom.
                    if (_inZoom == false || (DateTime.Now.Ticks - _lastClick) > MouseDragDelay) // (TimeSpan.FromSeconds(0.15).TotalMilliseconds * 10000)	
                    {
                        if (MapMouseDrag != null) MapMouseDrag(map, args);
                    }
                }
            }
        }

        #endregion


        #region MouseWheel Events

        ///<summary>
        /// Delegate for mouse wheel events.
        ///</summary>
        ///<param name="sender">Instance of Map</param>
        ///<param name="args">MapMouseWheelEventArgs</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public delegate void MapMouseWheelEvent(object sender, MapMouseWheelEventArgs args);

        /// <summary>
        /// Fires when the mouse wheel changes its value
        /// </summary>
        public event MapMouseWheelEvent MapMouseWheel;

        internal void MouseWheel(object sender, MapMouseWheelEventArgs args)
        {
            if (EnableMouseWheel)
            {
                var map = (Map)sender;
                if (MapMouseWheel != null) MapMouseWheel(map, args);
            }
        }
        #endregion


        #region View Events

        ///<summary>
        /// Delegate for generic DeepEarth.Client.MapControl event.
        ///</summary>
        ///<param name="sender">Instance of Map</param>
        ///<param name="args">MapEventArgs</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public delegate void MapEvent(object sender, MapEventArgs args);

        /// <summary>
        /// Fires when the silverlight application has finished loading
        /// </summary>
        public event MapEvent MapLoaded;

        /// <summary>
        /// Fires when the map changes its angle
        /// </summary>
        public event MapEvent MapRotationChanged;

        /// <summary>
        /// Fires when the UpdateModes changes. See GeometryLayer.cs for UpdateModes definition.
        /// </summary>
        public event MapEvent MapViewChanged;

        /// <summary>
        /// Fires when the UpdateModes changes, when significance of change is small. See GeometryLayer.cs for UpdateModes definition.
        /// </summary>
        public event MapEvent MapViewChangedEnded;

        /// <summary>
        /// Fires when the map initially starts to zoom in
        /// </summary>
        public event MapEvent MapZoomStarted;

        /// <summary>
        /// Fires when the map has finished zooming
        /// </summary>
        public event MapEvent MapZoomEnded;

        /// <summary>
        /// Fires when the base layer changes its tile provider
        /// </summary>
        public event MapEvent MapTileSourceChanged;

        /// <summary>
        /// Fires when the map drag behaviour changes
        /// </summary>
        public event MapEvent MapDragBehavourChanged;

        ///<summary>
        /// Delegate for defining the providing the ZoomChanged event.
        ///</summary>
        ///<param name="sender">Instance of Map</param>
        ///<param name="zoomLevel">double value for map zoom level</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public delegate void ZoomChanged(object sender, double zoomLevel);

        /// <summary>
        /// Fires when the zoom level of the map changes to any value
        /// </summary>
        public event ZoomChanged MapZoomChanged;

        private void RaiseZoomChanged(object sender, double zoom)
        {
            var map = (Map)sender;
            if (_lastZoom != zoom)
            {
                _lastZoom = zoom;
                var temp = MapZoomChanged;
                if (temp != null)
                {
                    MapZoomChanged(map, zoom);
                }
            }
        }

        internal void Loaded(object sender, MapEventArgs args)
        {
            var map = (Map)sender;
            if (MapLoaded != null) MapLoaded(map, args);
        }

        internal void RotationChanged(object sender, MapEventArgs args)
        {
            var map = (Map)sender;
            if (MapRotationChanged != null) MapRotationChanged(map, args);
        }

        internal void ViewChanged(object sender, MapEventArgs args)
        {
            var map = (Map)sender;

            //Note this operation exists to eliminate the high volume of insignificant updates from MSI.ViewUpdated
            //Uses the sum of squares to determine distance moved.
            //The MSI wobble effect seems to occur at ~ a .03% of the logical display.  
            //So setting the change tolerance at .05 eliminates much of the wobble effect especially post zoom.
            var distLogChange = Math.Sqrt(Math.Pow(_map.LogicalOrigin.X - _priorOrigin.X, 2) + Math.Pow(_map.LogicalOrigin.Y - _priorOrigin.Y, 2));
            var changeSignificance = 100 * distLogChange / _map.MapViewLogicalSize.Width;
           
            if (changeSignificance > ChangeSignificanceThreshold)
            {

                _priorOrigin = _map.LogicalOrigin;
                RaiseZoomChanged(map, map.ZoomLevel);
                if (MapViewChanged != null) MapViewChanged(map, args);
            }
        }

        internal void ZoomStarted(object sender, MapEventArgs args)
        {
            var map = (Map)sender;
            _inZoom = true;
            if (MapZoomStarted != null) MapZoomStarted(map, args);
        }

        internal void ViewChangedEnded(object sender, MapEventArgs args)
        {
            var map = (Map) sender;
            if (_inZoom)
            {
                _inZoom = false;
                if (MapZoomEnded != null) MapZoomEnded(map, args);
            }
            if (MapViewChangedEnded != null) MapViewChangedEnded(map, args);
        }

        internal void TileSourceChanged(object sender, MapEventArgs args)
        {
            var map = (Map)sender;
            if (MapTileSourceChanged != null) MapTileSourceChanged(map, args);
        }

        internal void DragBehavourChanged(object sender, MapEventArgs args)
        {
            var map = (Map)sender;
            if (MapDragBehavourChanged != null) MapDragBehavourChanged(map, args);
        }

        #endregion
    }
}